package com.zboin.framework.serve;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * <p>一个简单的缓存 k v 结构，并允许设置过期时间</p>
 * @param <K>
 * @param <V>
 */
@Slf4j
public class CacheMap<K, V> {
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    private static final long INTERVAL_MILLIS = 30 * 1000L;
    private static final String CACHE_NAME = "cache_map-";

    private final ConcurrentHashMap<K, CacheEntry<V>> map;
    private static final AtomicInteger counter = new AtomicInteger(1);
    private final String taskId;

    public CacheMap() {
        map = new ConcurrentHashMap<>(DEFAULT_INITIAL_CAPACITY);
        taskId = CACHE_NAME + counter.getAndIncrement();
        ScheduleServe.getInstance().scheduleAtFixedRate(taskId, this::expireAll,
                INTERVAL_MILLIS, INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
    }

    private static class CacheEntry<V> {
        /**
         * expire time:
         *  -1: never expire
         */
        static final int DEFAULT_EXPIRE = -1;
        private final long expire;

        V value;
        long expireAt;

        public CacheEntry(V value) {
            this(value, DEFAULT_EXPIRE);
        }

        public CacheEntry(V value, long expire) {
            this.value = value;
            this.expire = expire;
            this.expireAt = System.currentTimeMillis() + expire;
        }

        public boolean expired() {
            return expire > 0 &&  System.currentTimeMillis() > expireAt;
        }
    }

    private static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            @SuppressWarnings("removal")
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                    Thread.currentThread().getThreadGroup();
            namePrefix = "zboin-cache-" +
                    poolNumber.getAndIncrement() +
                    "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

    public void put(K key, V value) {
        this.put(key, value, CacheEntry.DEFAULT_EXPIRE);
    }

    public void put(K key, V value, long expire) {
        if (key == null || value == null) {
            throw new NullPointerException("cache map not allowed null value");
        }
        this.map.put(key, new CacheEntry<>(value, expire));
    }

    private CacheEntry<V> getEntry(K key) {
        CacheEntry<V> entry = this.map.get(key);
        if (entry == null) {
            return null;
        }
        if (entry.expired()) {
            return null;
        }
        return entry;
    }

    public V get(K key) {
        CacheEntry<V> entry = getEntry(key);
        return entry == null ? null : entry.value;
    }

    public long getExpireAt(K key) {
        CacheEntry<V> entry = getEntry(key);
        if (entry == null) return -1;
        return entry.expired() ? -1 : entry.expireAt;
    }

    public void remove(K key) {
        this.map.remove(key);
    }

    public void renewExpire(K key, long expire) {
        CacheEntry<V> entry = getEntry(key);
        if (entry == null) return;
        synchronized (entry) {
            this.map.remove(key);
            this.map.put(key, new CacheEntry<>(entry.value, expire));
        }
    }

    public void clear() {
        this.map.clear();
    }

    public int size() {
        return this.map.size();
    }

    public boolean containsKey(K key) {
        return this.map.containsKey(key);
    }

    public void expireAll() {
        this.map.entrySet().removeIf(e -> e.getValue().expired());
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            ScheduleServe.getInstance().cancelTask(taskId, true);
        } finally {
            super.finalize();
        }
    }
}
